﻿using System;
using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Utility/facade class that simplifies common security logic.</summary>
    public static class SecurityUtil
    {
        #region Security provider facade.

        /// <summary>Gets security provider for the given entity.</summary>
        /// <param name="entity">Entity.</param>
        /// <returns>Specialized provider for the specified entity type or <see cref="SecurityProviderFactory.DefaultProvider"/> if no special provider is defined for the requested type.</returns>
        public static IEntitySecurityProvider GetProvider(IBusinessObject entity)
        {
            return SecurityProviderFactory.GetProvider(entity.Table.TableName);
        }

        /// <summary>Gets security provider for the given entity.</summary>
        /// <param name="entityTable">Table that contains entities for which security is to be enforced.</param>
        /// <returns>Specialized provider for the specified entity type or <see cref="SecurityProviderFactory.DefaultProvider"/> if no special provider is defined for the requested type.</returns>
        public static IEntitySecurityProvider GetProvider(IDbTable entityTable)
        {
            return SecurityProviderFactory.GetProvider(entityTable.TableName);
        }

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="entityTable">Table that contains entities for which security is to be enforced.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="query">Query to which join clauses and additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>
        public static void ApplyAccessFilterToQuery(IBusinessObjectDescriptor descriptor, IDbTable entityTable, IActor user, SelectStatement query, AccessMode accessMode, AccessFilterOptions options)
        {
            GetProvider(entityTable).ApplyAccessFilterToQuery(descriptor, user, query, accessMode, options);
        }

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="entityTable">Table that contains entities for which security is to be enforced.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="engine">Search engine to which join clauses and additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>
        public static void ApplyAccessFilterToSearchEngine(IBusinessObjectDescriptor descriptor, IDbTable entityTable, IActor user, SearchEngine engine, AccessMode accessMode, AccessFilterOptions options)
        {
            GetProvider(entityTable).ApplyAccessFilterToSearchEngine(descriptor, user, engine, accessMode, options);
        }

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="entityTable">Table that contains entities for which security is to be enforced.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="sourceTable">Table which contains the entities. Aliases are permitted.</param>
        /// <param name="relations">Relation bucket to which join clauses will be added as required.</param>
        /// <param name="where">Search condition to which additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>
        public static void ApplyAccessFilterToQuery(IBusinessObjectDescriptor descriptor, IDbTable entityTable, IActor user, IDbTable sourceTable, RelationBucket relations, SearchCondition where, AccessMode accessMode, AccessFilterOptions options)
        {
            GetProvider(entityTable).ApplyAccessFilterToQuery(descriptor, user, sourceTable, relations, where, accessMode, options);
        }

        /// <summary>Check whether the user can open/read the specified entity. Does NOT implement offline concurrency control.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to access.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public static bool CanReadEntity(IActor user, IBusinessObject entity)
        {
            return GetProvider(entity).CanReadEntity(user, entity);
        }

        /// <summary>Check whether the user can acces and save the specified entity. Does NOT implement offline concurrency control.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to save.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public static bool CanSaveEntity(IActor user, IBusinessObject entity)
        {
            return GetProvider(entity).CanSaveEntity(user, entity);
        }

        /// <summary>Check whether the user can acces and delete/deactivate the specified entity. Does NOT implement offline concurrency control.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to delete.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public static bool CanDeleteEntity(IActor user, IBusinessObject entity)
        {
            return GetProvider(entity).CanDeleteEntity(user, entity);
        }

        /// <summary>Fetches actor info associated with the provided username.</summary>
        /// <param name="username">Username.</param>
        /// <returns><see cref="IActor"/> or <b>null</b> it the specified username cannot be found.</returns>
        public static IActor FetchActorInfo(string username)
        {
            return SecurityProviderFactory.DefaultProvider.FetchActorInfo(username);
        }

        /// <summary>Returns <b>true</b> if entity class implement record ownership, ie. if it has owner ID or team ID or unit ID field.</summary>
        /// <param name="entity">Business object.</param>
        /// <returns><b>true</b> if entity has at least one ownership field defined in its descriptor; otherwise <b>false</b></returns>
        public static bool ImplementsRecordOwnership(IBusinessObject entity)
        {
            return GetProvider(entity).ImplementsRecordOwnership(entity);
        }

        #endregion

        #region Check*Permission.

        /// <summary>
        /// Checks if actor has the specified permission.
        /// </summary>
        /// <param name="actor">Action initiator which should contains the specified permission.</param>
        /// <param name="permissionId">The ID of the action that the actor is trying to initiate.</param>
        /// <param name="ruleKey">Optional <see cref="BusinessRule.Name"/>.</param>
        /// <param name="ruleDescriptions">Message source which contains the description of the specified rules.
        /// If a string with <b>ruleKey</b> is not found, a default description is used.</param>
        /// <returns>Empty collection if actor has permission; otherwise a collection with one broken rule.</returns>
        public static BusinessRuleCollection CheckPermission(IActor actor, object permissionId, string ruleKey, ITextSource ruleDescriptions)
        {
            string description = null;
            if (!string.IsNullOrEmpty(ruleKey))
                description = ruleDescriptions.GetString(ruleKey);
            if (string.IsNullOrEmpty(description))
                description = GetPermissionDeniedDescription(ruleDescriptions);

            BusinessRuleCollection permissions = new BusinessRuleCollection();
            permissions.Add(new BusinessRule(ruleKey, actor.HasPermission(permissionId), description));
            return permissions.GetBrokenRules();
        }

        /// <summary>
        /// Checks whether actor has any of the specified permissions.
        /// </summary>
        /// <param name="actor">Action initiator which should contains the specified permission.</param>
        /// <param name="ruleKey">Optional <see cref="BusinessRule.Name"/>.</param>
        /// <param name="ruleDescriptions">Message source which contains the description of the specified rules.
        /// If a string with <b>ruleKey</b> is not found, a default description is used.</param>
        /// <param name="permissionIDs">IDs of actions that the actor is trying to initiate.</param>
        /// <returns>Empty collection if actor has permission; otherwise a collection with one broken rule.</returns>
        public static BusinessRuleCollection CheckAnyPermission(IActor actor, string ruleKey, ITextSource ruleDescriptions, params object[] permissionIDs)
        {
            string description = null;
            if (!string.IsNullOrEmpty(ruleKey))
                description = ruleDescriptions.GetString(ruleKey);
            if (string.IsNullOrEmpty(description))
                description = GetPermissionDeniedDescription(ruleDescriptions);

            BusinessRuleCollection permissions = new BusinessRuleCollection();
            bool hasPermission = false;
            foreach (object permId in permissionIDs)
            {
                if (actor.HasPermission(permId))
                {
                    hasPermission = true;
                    break;
                }
            }

            permissions.Add(new BusinessRule(ruleKey, hasPermission, description));
            return permissions.GetBrokenRules();
        }

        #endregion

        #region Check read/write permissions + pessimistic concurrency control.

        /// <summary>Checks if actor can read/fetch the provided entity. Implements pessimistic offline concurrency control, i.e. returns one broken rule if other user/process has exclusively locked the object.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to access.</param>
        /// <param name="ruleKey">Text source key and business rule name. If null then default rule description will be created.</param>
        /// <returns>Empty collection if actor has required permissions; otherwise a collection with one broken rule.</returns>
        public static BusinessRuleCollection CheckReadPermission(IActor user, IBusinessObject entity, string ruleKey)
        {
            var lockingRules = LockUtil.CheckReadLock(user, entity);
            if (lockingRules.HasBrokenRules)
                return lockingRules;

            string description = GetBusinessRuleDescription(entity, ruleKey);
            bool hasPermission = CanReadEntity(user, entity);
            var rules = new BusinessRuleCollection(new BusinessRule(ruleKey, hasPermission, description));
            return rules.GetBrokenRules();
        }

        /// <summary>Checks if actor can save the provided entity. Implements pessimistic offline concurrency control, i.e. returns one broken rule if other user/process has locked the object.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to access.</param>
        /// <param name="ruleKey">Business rule key. The method will try to find the rules description in the messace source with that key.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public static BusinessRuleCollection CheckSavePermission(IActor user, IBusinessObject entity, string ruleKey)
        {
            var lockingRules = LockUtil.CheckWriteLock(user, entity);
            if (lockingRules.HasBrokenRules)
                return lockingRules;

            string description = GetBusinessRuleDescription(entity, ruleKey);
            bool hasPermission = CanSaveEntity(user, entity);
            var rules = new BusinessRuleCollection(new BusinessRule(ruleKey, hasPermission, description));
            return rules.GetBrokenRules();
        }

        /// <summary>Checks if actor can delete/deactivate the provided entity. Implements pessimistic offline concurrency control, i.e. returns one broken rule if other user/process has locked the object.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to access.</param>
        /// <param name="ruleKey">Business rule key. The method will try to find the rules description in the messace source with that key.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public static BusinessRuleCollection CheckDeletePermission(IActor user, IBusinessObject entity, string ruleKey)
        {
            var lockingRules = LockUtil.CheckWriteLock(user, entity);
            if (lockingRules.HasBrokenRules)
                return lockingRules;

            string description = GetBusinessRuleDescription(entity, ruleKey);
            bool hasPermission = CanSaveEntity(user, entity);
            var rules = new BusinessRuleCollection(new BusinessRule(ruleKey, hasPermission, description));
            return rules.GetBrokenRules();
        }

        private static string GetBusinessRuleDescription(IBusinessObject entity, string ruleKey) 
        {
            ITextSource ruleDescriptions = ((ISelfValidatingObject)entity).MessageSource;
            string description = null;
            if (!string.IsNullOrEmpty(ruleKey))
                description = ruleDescriptions.GetString(ruleKey);
            if (string.IsNullOrEmpty(description))
                description = GetPermissionDeniedDescription(ruleDescriptions);
            return description;
        }

        #endregion

        /// <summary>Specifies the key in a <see cref="ITextSource"/> which should contain default "permission denied" message.
        /// Default is <b>_SecurityUtil_PermissionDenied</b>.</summary>
        /// <value>"_SecurityUtil_PermissionDenied"</value>
        static readonly string[] PossiblePermissionDeniedDescriptionKeys = new string[] 
        { 
            "_SecurityUtil_PermissionDenied",
            // Legacy keys.
            "_GrantHelper_PermissionDenied",
            "GrantHelper_PermissionDenied"
        };

        /// <summary>
        /// Gets description which is used if a <see cref="ITextSource"/> doesn't contain the <b>_SecurityUtil_PermissionDenied</b> key.
        /// </summary>
        /// <value>English: "You don't have the required permission to execute the requested action."</value>
        public static string DefaultPermissionDeniedDescription
        {
            get { return Messages.GrantHelper_DefaultPermissionDeniedDescription; }
        }

        /// <summary>
        /// Gets the default "permission denied" message from the specified source.
        /// </summary>
        /// <param name="ruleDescriptions">Message source which contains the string with the <b>_SecurityUtil_PermissionDenied</b> key.</param>
        /// <returns>A message with the <b>_SecurityUtil_PermissionDenied</b> key or <see cref="DefaultPermissionDeniedDescription"/> if not found.</returns>
        public static string GetPermissionDeniedDescription(ITextSource ruleDescriptions)
        {
            string description = TryGetPermissionDeniedDescriptionFromTextSource(ruleDescriptions);
            return string.IsNullOrEmpty(description) ? DefaultPermissionDeniedDescription : description;
        }

        private static string TryGetPermissionDeniedDescriptionFromTextSource(ITextSource ruleDescriptions)
        {
            foreach (string key in PossiblePermissionDeniedDescriptionKeys)
            {
                string description = ruleDescriptions.GetString(key);
                if (!string.IsNullOrEmpty(description))
                    return description;
            }

            return null;
        }

        #region EnforceRules.

        /// <summary>
        /// Throws <see cref="BrokenRuleException"/> if the provided method returns a collection with broken rules.
        /// </summary>
        /// <param name="condition">If <b>true</b> the provided method is executed and rules are evaluate.</param>
        /// <param name="grantMethod">A method which checks the rules for the specified actor.</param>
        /// <param name="actor">Actor fow which the rules are to be evaluated if <b>condition</b> parameter is set to <b>true</b>.</param>
        public static void EnforceRules(bool condition, Delegates.Function<BusinessRuleCollection, IActor> grantMethod, IActor actor)
        {
            if (condition)
            {
                BusinessRuleCollection rules = grantMethod(actor);
                if (rules.HasBrokenRules)
                    throw new BrokenRuleException(rules);
            }
        }

        #endregion
    }
}